﻿<#
    Script  : ImportPSModule
    Author  : Riwut Libinuko
    Blog    : http://blog.libinuko.com 
    Copyright :© IdeasFree - cakriwut@gmail.com, 2011. All rights reserved.
#>

<# 
   Installation and Configuration
   1. Edit profile.ps1 to load ImportPSModule on start.
   2. You can change $globalPkgSource to local repository if necessary.
   3. Publish your PowerShell Module as NuGet package, tag with at least PSModule.
#>
$globalPkgSource = "https://go.microsoft.com/fwlink/?LinkID=230477"

#Auto configuration
$basePath = (split-path -parent $MyInvocation.MyCommand.Definition)
$userModulePath = $env:PSModulePath -split ";"
$isModule = $userModulePath |? { $basePath.StartsWith($_) } 
$thisNuspec =Join-Path ($basePath | Split-Path ) "ImportPSModule.nuspec"

$globalPkgInstallationDir = Join-Path $basePath "Modules"
$globalPkgDistDir = Join-Path $env:temp "Distribution"  

$webClient = New-Object System.Net.WebClient 
$webClient.UseDefaultCredentials = $true
$webClient.Proxy.Credentials = $webClient.Credentials
#End autoconfiguration
gci $psscriptroot\*.ps1 | % { . $_.FullName }

function GetNugetPath
{
  #Check if Nuget.Exe is available locally
  $nugetPath = (gci $basePath -recurse -filter "nuget.exe").FullName
    
  if(!$nugetPath)
  {	 
    # Download latest Nuget.exe commandline to installation directory
	$installDir = $null
	if($matches) {
       $matches.Clear()
     }
     if($isModule)     
     {     
        if(!($nugetPath = (gci $userModulePath[0] -recurse -filter "nuget.exe" | sort LastWriteTime -Descending | select -First 1).FullName))
        {
           $installDir = $userModulePath[0] # Nuget.exe can not run from within GlobalModule. A bug or design limitation??
        } else {        
           return $nugetPath
        }
     } else {
         $installDir = $basePath        
     }
	 
	$nugetPath = GetLatestNugetExe -installLocation $installDir
	if(!$nugetPath)
         {
           Write-Host "Can not find mandatory Nuget.exe"           
         } else 
         {
            return $nugetPath
         }  
		 
  }  
  return $nugetPath
}

function GetNuspecContent
{
    param (
	   $packageFullPath
	)
	
	if((gi $packageFullPath |? {$_.extension -eq ".nupkg" }))
	{
	  $zipName = [IO.Path]::ChangeExtension($packageFullPath,".zip")
	  Copy-Item $packageFullPath $zipName -Force | Out-Null
	  $shellApp = New-Object -Com Shell.Application
	  $zipContent = $shellApp.namespace($zipName)
	  $dest = $shellApp.namespace((Split-Path -Parent $packageFullPath))
	  $dest.Copyhere(($zipContent.Items() |? { $_.Name -match ".*\.nuspec`$" }),16)
	  $nuspec = (gci (Split-Path -Parent $packageFullPath) -Filter "*.nuspec" |  Sort-object "LastWriteTime" -Descending | Select-Object -First 1)
	  $nuspecXml = [xml] (gc $nuspec.FullName)  
	  Remove-Item $zipName -Force | Out-Null	  
	  return New-Object PSObject -Property @{
                           FileName = $nuspec.FullName;
                           ContentXml = $nuspecXml} 
	}
}

function GetDependencyVersion
{
   param (
       $dependendPkg
   )
   [xml] $nuspecXml = $null;
   
   if((Test-Path $thisNuspec))
   {
      $nuspecXml = [xml] (gc $thisNuspec)
   } else {
      $psModulePkg = gci (Split-Path -Parent $basePath ) -Filter "*.nupkg" | Sort-Object -Descending | Select-Object -First 1
	  $nsInfo = GetNuspecContent $psModulePkg.FullName  	  
	  Remove-Item $nsInfo.FileName | Out-Null
	  $nuspecXml = $nsInfo.ContentXml
   }
   
   return ($nuspecXml.package.metadata.dependencies.dependency |? {
	                       $_.Id -eq "$dependendPkg" }).version
}

function CheckNugetVersion
{
    param (
	   $nugetPath,
	   $minVersion
	)
	
	$version = Invoke-Expression "& `"$nugetPath`"" |? {
	             $_ -match "Nuget Version:.\s?(?<Version>.*)$"  } |% {
				          $matches["Version"] -split "\." }
	
	$validVersion = $minVersion -split "\." 
	for($i=0;$i -lt $validVersion.Length;$i++)
	{
	   if($version[$i] -gt $validVersion[$i])
	   {
	       return $true
	   } elseif ($version[$i] -lt $validVersion[$i])
	   {
	      return $false
	   }
	}
	return $true
}

function Unzip($zipFile, $dest)
{    
    new-item $dest -ItemType Directory -force | out-null
    $shellApp = New-Object -Com Shell.Application      
    $ZipFileRef = $shellApp.namespace([String]$zipFile)             
    $destination = $shellApp.namespace($dest)             
    $destination.Copyhere($ZipFileRef.items(),16)
 }

function CompareVersion
{
   param ($firstVersion,$secondVersion)
   
   $leftVersion = $firstVersion -split "\."
   $rightVersion = $secondVersion -split "\."

   for($i=0;$i -lt $leftVersion.Length;$i++)
   {
       if($leftVersion[$i] -lt $rightVersion[$i])
	   {
	      return -1
	   } elseif ($leftVersion[$i] -gt $rightVersion[$i])
	   {
	      return 1
	   }
   }
   if($leftVersion.Length -lt $rightVersion.Length)
   {
      return -1
   }
   return 0
}

function Import-PSModule
{ 
<#
.SYNOPSIS
Import-PSModule from central NuGet repository into memory. 
		
.DESCRIPTION
The function import and load PowerShell module from NuGet repository. It can register the new module to current/global user profile, 
so that the module will be available on any future session.
    
.INPUTS
None. You can not pipe objects to Import-PSModule.
	
.OUTPUTS
None. Operation status,new module name and exported commands are displayed in the screen.
		
.PARAMETER PackageName
MANDATORY parameter to specify the PackageName. For example RemoteStsAdm. 
	
.PARAMETER Source
OPTIONAL parameter to specify NuGet repository URL. Default: https://go.microsoft.com/fwlink/?LinkID=206669. 
You can override this value by specifying global variable $globalPkgSource of Import-PSModule. 
        
.PARAMETER Version
OPTIONAL parameter to load PS Module from specific Package version. This value will be ignored if Latest = TRUE.

.PARAMETER Latest
OPTIONAL parameter to load PS Module from latest Package version.

.PARAMETER Tags
OPTIONAL parameter to identify special NuGet package tags. Default : PSModule.

.PARAMETER Install
OPTIONAL parameter to specify install/un-install operation. Default : TRUE.
			

.EXAMPLE
	PS>  Import-PSModule RemoteStsAdm 
			

	Description
	-----------
	Download, extract and register PS Module from RemoteStsAdm package stored in default NuGet repository.
		
.EXAMPLE
	PS> Import-PSModule -PackageName RemoteStsAdm -Source  "http://code.contoso.com/nuget"


	Description
	-----------
	Download, extract and register PS Module from RemoteStsAdm package stored in http://http://code.contoso.com/nuget.
            
.LINK
    Author blog  : IdeasForFree  (http://blog.libinuko.com)
.LINK
    Author email : cakriwut@gmail.com
#> 
   [CmdletBinding(DefaultParameterSetName="Install")]
   param(
      [Parameter(Mandatory=$true,Position=0,ParameterSetName="Install")]
	  [Parameter(Mandatory=$false,ParameterSetName="Update")]
      [string] $packageName,      
      [Parameter(Mandatory=$false,Position=1,ParameterSetName="Install")]
	  [Parameter(Mandatory=$false,ParameterSetName="Update")]
	  [Parameter(ParameterSetName="List")]
      [string] $source= $globalPkgSource,
	  [Parameter(ParameterSetName="Install")]
      [string] $version =$null,
	  [Parameter(ParameterSetName="Install")]
      [bool] $latest= $true,
	  [Parameter(ParameterSetName="Install")]
	  [Parameter(ParameterSetName="List")]
      [string] $tags='PSModule',
	  [Parameter(ParameterSetName="Install")]
      [bool] $install = $true,
	  [Parameter(ParameterSetName="List")]
	  [switch] $ListAvailable,
	  [Parameter(ParameterSetName="Update")]
	  [switch] $Update,
	  [Parameter(ParameterSetName="UpdateAll")]
	  [switch] $UpdateAll,
	  [Parameter(ParameterSetName="UpdateSelf")]
	  [switch] $UpdateSelf
   )

   switch ($PsCmdLet.ParameterSetname)
   {
      "Install" {   Import-PSModule-Install -packageName $packageName -source $source -version $version -latest $latest -tags $tags -install $install     }
	  "Update"	{	Write-Host "Not yet implemented"	}
	  "List"	{	Import-PSModule-List -tags $tags -source $source	}
	  "Update"	{	Write-Host "Not yet implemented"	}
	  "UpdateAll" {	Write-Host "Not yet implemented"	}
	  "UpdateSelf" { 
	  	  Import-PSModule-List -tags $tags -source $source |? {$_.Name -eq "ImportPSModule" -and $_.Status -eq "Need Upgrade"}	
		  Import-PSModule-Install -packageName "ImportPSModule" -source $source -latest $true -install $true -installDir "$basePath\..\.."
		  Write-Host $location
		}
   }      
}


function Register-PSModule
{
<#
.SYNOPSIS
Locate and register all PS Module from specific path. 
		
.DESCRIPTION
Registering all PS Module from given path.
    
.INPUTS
None. You can not pipe objects to Import-PSModule.
	
.OUTPUTS
None. Operation status,new module name and exported commands are displayed in the screen.
		
.PARAMETER ModuleDirectory
MANDATORY parameter in which all PS Module in ModuleDirectory will be loaded (and/or registered). 			

.EXAMPLE
	PS>  Register-PSModule "D:\LocalModule"
			

	Description
	-----------
	Register all PS Module in D:\LocalModule and its sub folder.
		            
.LINK
    Author blog  : IdeasForFree  (http://blog.libinuko.com)
.LINK
    Author email : cakriwut@gmail.com
#> 

   param (
      [Parameter(Mandatory=$true)]
      [string] $ModuleDirectory      
   )
                             
   if(!($moduleLists = GetAllModuleFiles $ModuleDirectory))
   {
      Write-Warning "Can not find any modules. Check if PowerShell module exists in $ModuleDirectory"
      return 
   }

   $package = GetPackageInfo $ModuleDirectory
   
   $single = new-object System.Management.Automation.Host.ChoiceDescription "Once","Load module for this session only"
   $personal = new-object System.Management.Automation.Host.ChoiceDescription "Personal","Load and register module in PowerShell current user profile"
  # $global = new-object System.Management.Automation.Host.ChoiceDescription "Global","Load and register module in PowerShell ALL user profile"
   $options = [System.Management.Automation.Host.ChoiceDescription[]]($single,$personal) #,$global)
   
   $selectedStatus = $host.ui.PromptForChoice("New-PSModule Registration","Do you want to register the module?",$options,1)   
   
   if($selectedStatus -eq 0)
   {  
     # No update required      
   } elseif ($selectedStatus -eq 1)
   {
      Write-Host "Register personal $($package.Name) modules"
      Update-Profile -profilePath $profile.CurrentUserAllHosts -PSModuleName $package.Name -PSModuleVer $package.Version -Content $moduleLists
      Update-Profile -profilePath $profile.CurrentUserCurrentHost -PSModuleName $package.Name -PSModuleVer $package.Version -Content $moduleLists  
                 
   } else {
      Write-Host "Register global $($package.Name) modules"
      #UpdateProfile -profilePath $profile.AllUsersAllHosts -PSModuleName $package.Name -PSModuleVer $package.Version -Content $moduleLists
      #UpdateProfile -profilePath $profile.AllUsersCurrentHost -PSModuleName $package.Name -PSModuleVer $package.Version -Content $moduleLists  
   }
   
   $moduleLists |% {     
      if(get-module -name $_.ModuleName)
      {
          remove-module -name $_.ModuleName
       }
       Write-Host "..Loading module $($_.ModuleName)" -NoNewLine
       Import-Module -name $_.ModulePath -global
       Write-Host "..Sucessful." -foregroundcolor DarkGreen
       Get-Module $_.ModuleName
   } 
}

function GetAllModuleFiles
{
    param (
      [string] $ModuleDirectory
    )
    
	if(($psd = ( gci $ModuleDirectory -recurse -filter "*.psd1" |% {
               new-object PSObject -prop @{ 
                       ModuleName=$_.BaseName; 
                       ModulePath=(join-path $_.DirectoryName $_.BaseName) }
              })) -ne $null)
    {
	   return $psd
	} else {
      return ( gci $ModuleDirectory -recurse -filter "*.psm1" |% {
               new-object PSObject -prop @{ 
                       ModuleName=$_.BaseName; 
                       ModulePath=(join-path $_.DirectoryName $_.BaseName) }
              })
   }
}




function PreparePath
{
    param (
        [string] $path
    )
    
    if(!(test-path $path))
    {
       new-item -type directory -path $path -force     
    }
    return $path
}

function GetPackage
{
    param(
        [string] $packageName,        
        [string] $source= $globalPkgSource,
        [string] $distFolder = $globalPkgDistDir,
        [bool] $latest=$true,
        [string] $version,
        [string] $extrafilter=$null        
    )
    
    # set up feed URL    
    $serviceBase = GetPackageUrl $source
    $feedUrl = $serviceBase + "Packages"

    if($latest) {
        $feedUrl = $feedUrl +"?`$filter=(IsLatestVersion eq true) and (Id eq `'$packageName`')"
    } else {
        $feedUrl = $feedUrl + "?`$filter=(Version eq $version) and (Id eq `'$packageName`')"
    }
    
    if($extrafilter)
    {
       $feedUrl = $feedUrl + " and $extrafilter"
    }

    PreparePath $distFolder | Out-Null
    Write-Host "Download package $packageName" -NoNewLine
    DownloadEntries $feedUrl $distFolder | Out-Null
    if(($pkgPath = gci $distFolder |? { $_.Name -match "$packageName\." } | sort LastWriteTime -Descending | select -First 1))
    {
       write-host "..Sucessful" -foreground DarkGreen
       return $pkgPath.FullName
    } else {
       write-host "..Unsucessful." -foreground red
       write-host "..Error: Can not download package $packageName" -foreground red
       return $null
    }
}

function GetPackageUrl 
{  
   param ([string]$source)  
   $resp = [xml]$webClient.DownloadString($source)  
   return $resp.service.GetAttribute("xml:base") 
}

function InstallPkg
{
<#
.SYNOPSIS
Install package from specific location. 
		
.DESCRIPTION
Instal package from specific location.
    
.INPUTS
None. You can not pipe objects to InstallPkg.
	
.OUTPUTS
None. 
		
.PARAMETER PackageName
MANDATORY parameter . 			

.PARAMETER Source
MANDATORY parameter .

.PARAMETER InstallDir
MANDATORY parameter .


.EXAMPLE
	PS>  InstallPkg RemoteStsAdm "D:\LocalModule"
			

	Description
	-----------
	Install RemoteStsAdm from D:\LocalModule.
		            
.LINK
    Author blog  : IdeasForFree  (http://blog.libinuko.com)
.LINK
    Author email : cakriwut@gmail.com
#> 
    param (
       [Parameter(Mandatory=$true,Position=0)]
       [string] $packageName,
       [Parameter(Mandatory=$false,Position=1)]
       [string] $source = $globalPkgSource,
       [string] $installDir = $globalPkgInstallationDir,
       [bool] $nodeps = $false
    )
    write-host "Trying to extract package $packageName" -NoNewLine
    
    #Nuget.exe compensation. Can not install/read Global PSModule path
    if($installDir.StartsWith($userModulePath[1]))
    {
       $installDir = $installDir.Replace($userModulePath[1].TrimEnd("\"),$userModulePath[0].TrimEnd("\"))
    }

    $nugetExe = GetNugetPath   
    $nugetArgs = "install $packageName -source `"$source`" -OutputDirectory `"$installDir`" -excludeversion"
    $result = invoke-expression "& `"$nugetExe`" $nugetArgs 2>&1"     
    $pattern = "Unable to resolve dependency \'(?<Pkg>.+)\s?\(\≥(?<Ver>.+)\)\'"
    $unresolved = $result |? {
                      $_ -match $pattern } |% { 
                           new-object PSObject -prop @{ Package=($matches['Pkg']).Trim(); Version=($matches["Ver"]).Trim() }}                               
    $localSource = ( $source |? { $_ -match "(http|https)\:\/\/.+?" }) -eq $null    
    
    if($unresolved -eq $null)
    {
      if(($result |? { $_ -match "Unable to find package \'(.+?)\'" }))
      {
         write-host "..Unsucessful." -foregroundcolor red
         write-host "..Error: $result" -foregroundcolor red         
      } else {
         write-host "..Successful." -foregroundcolor DarkGreen
         $pkgLocation = gci $installDir |? { $_.Name -match "$packageName(\.(\d\.){1,}(\d))?`$" } | sort name -Descending | select -First 1
         return (join-path $installDir $pkgLocation)
      }
    } elseif ($nodeps)
    {
      write-host "..Unsucessful."  -foregroundcolor red 
      write-host "..Error: $result" -foregroundcolor red      
    } else {       
      $unresolved | foreach { 
        write-host ""
        write-host "..Downloading dependencies $($_.Package) ($($_.Version) or later)"
        $depPkg=$null
        # Call to download pkg
        if($localSource)
        {
           $depPkg = GetPackage -Packagename $_.Package -distFolder $source
        } else {
           # This rarely happened because Nuget will handle itself.
           $depPkg = GetPackage -Packagename $_.Package
        }
        # Call to install pkg
        if($depPkg)
        {
           InstallPkg -PackageName $_.Package -Source $source   
           $nodeps = $false         
        } else {
           $nodeps = $true
        }
      }
      #Final to install current package. $nodeps
      InstallPkg -PackageName $packageName -Nodeps $nodeps -Source $source
    }
}

function GetPackageFeed
{
   param (
     [string] $feedUrl
   )
   
  $feed = [xml]$webClient.DownloadString($feedUrl) 
  $entries = $feed | select -ExpandProperty feed | select -ExpandProperty entry -ErrorAction SilentlyContinue  
  if(!$entries)
  {      
	return $null
  } else {
    return $entries
  }
}
function DownloadEntries 
{  
  param (
      [string]$feedUrl,
      $destinationDirectory
  )    
  $feed = [xml]$webClient.DownloadString($feedUrl) 
  $entries = $feed | select -ExpandProperty feed | select -ExpandProperty entry -ErrorAction SilentlyContinue  
  if(!$entries)
  {      
     return "Can not find any matched packages."
  }
  $progress = 0                
  foreach ($entry in $entries) 
  {     
    $url = $entry.content.src   
    if($entry.properties.id)
    {  
      $fileName = $entry.properties.id + "." + $entry.properties.version + ".nupkg"    
    } else {
      $fileName = $entry.title."#text" + "." + $entry.properties.version + ".nupkg" 
    }
    
    $saveFileName = join-path $destinationDirectory $fileName       
    $pagepercent = ((++$progress)/@($entries).Length*100)     
    if ((-not $overwrite) -and (Test-Path -path $saveFileName))     
    {         
        #write-progress -activity "$fileName already downloaded, using cached file" -status "$pagepercent% of current page complete" -percentcomplete $pagepercent        
		Write-Host "..$fileName already downloaded, using cached file.." -NoNewline
		#Write-Host "$pagepercent% complete" -ForegroundColor DarkGreen
        continue     
    }     
    #write-progress -activity "Downloading $fileName" -status "$pagepercent% of current page complete" -percentcomplete $pagepercent    
	Write-Host "..Downloading $fileName .." -NoNewline
	#Write-Host "$pagepercent% complete" -ForegroundColor DarkGreen
    $webClient.DownloadFile($url, $saveFileName)   
  }   
  
  $link = $feed.feed.link | where { $_.rel.startsWith("next") } | select href   
  if ($link -ne $null) 
  {         
    $feedUrl = $link.href     
    DownloadEntries $feedUrl $destinationDirectory
  } 
} 

GetNugetPath | out-null